/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.polarrose.spring.bayeux.util;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.WildcardType;
import java.lang.reflect.ParameterizedType;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.*;

/**
 * <p>Utility class to convert objects to and from the JSON representation that Jetty uses internally
 * for their Bayeux implementation. This class is a little smarter than Jetty's {@link org.mortbay.util.ajax.JSON}
 * class because it knows about more types and can also convert from and to Java Beans.</p>
 *
 * <p>It follows the following rules for mapping JSON to Java:</p>
 *
 * <pre>
 *   object ==> Map
 *   array  ==> Object[]
 *   number ==> Double or Long
 *   string ==> String
 *   null   ==> null
 *   bool   ==> Boolean
 * </pre>
 *
 * <p>And the follwing rules for mapping Java to JSON:</p>
 *
 * <pre>
 *   String --> string
 *   Number --> number
 *   Map    --> object
 *   List   --> array
 *   Array  --> array
 *   null   --> null
 *   Boolean--> boolean
 *   Simple String Type --> string
 *   JavaBean --> map
 *   Object --> string
 * </pre>
 *
 * @author Stefan Arentz
 */

public class JsonUtil
{
    private final static Log log = LogFactory.getLog(JsonUtil.class);

    private static final Set<Class<?>> simpleTypes = new HashSet<Class<?>>();

    static {
        simpleTypes.add(Boolean.class);
        simpleTypes.add(Byte.class);
        simpleTypes.add(Character.class);
        simpleTypes.add(Short.class);
        simpleTypes.add(Integer.class);
        simpleTypes.add(Long.class);
        simpleTypes.add(Float.class);
        simpleTypes.add(Double.class);
    }

    private static final Set<Class<?>> simpleStringTypes = new HashSet<Class<?>>();

    static {
        simpleStringTypes.add(String.class);
        simpleStringTypes.add(UUID.class);
        simpleStringTypes.add(URL.class);
    }

    private static final Package javaPackage = Package.getPackage("java");private JsonUtil() {
    }

    /**
     *
     * @param object
     * @return
     */

    public static Object transmogrify(Object object)
    {
        if (object == null) {
            return null;
        } else {
            return serializeObject(object);
        }
     }

    //

    private static Object serializeObject(Object object)
    {
        Class<?> clazz = object.getClass();

        if (simpleTypes.contains(clazz)) {
            return serializeSimpleType(object);
        }

        if (simpleStringTypes.contains(clazz)) {
            return serializeSimpleStringType(object);
        }

        if (clazz.isPrimitive()) {
            return serializePrimitiveType(object);
        }

        if (clazz.isArray()) {
            return serializeArray(object);
        }

        if (object instanceof Collection) {
            return serializeCollection((Collection) object);
        }

        if (object instanceof Map) {
            return serializeMap((Map) object);
        }

        if (clazz.isEnum()) {
            return serializeEnum((Enum) object);
        }

        else if (!clazz.getPackage().equals(javaPackage)) {
            return serializeJavaBean(object);
        }

        return null;
    }

    private static Object serializeEnum(Enum e)
    {
        return e.toString();
    }

    private static Object serializeSimpleType(Object object)
    {
        if (object.getClass() == Long.class || object.getClass() == Double.class) {
            return object;
        } else {
            return object.toString();
        }
    }

    private static Object serializeSimpleStringType(Object object)
    {
        return object.toString();
    }

    private static Object serializePrimitiveType(Object object)
    {
        return object;
    }

    private static Object serializeArray(Object object)
    {
        Object[] array = new Object[Array.getLength(object)];

        for (int i = 0; i < Array.getLength(object); i++) {
            array[i] = serializeObject(Array.get(object, i));
        }

        return array;
    }

    private static Object serializeCollection(Collection collection)
    {
        Object[] array = new Object[collection.size()];

        int i = 0;
        for (Object o : collection) {
            array[i++] = serializeObject(o);
        }

        return array;
    }

    private static Object serializeMap(Map map)
    {
        Map<String,Object> result = new HashMap<String,Object>();

        for (Object key : map.keySet()) {
            if (simpleTypes.contains(key.getClass()) || simpleStringTypes.contains(key.getClass())) {
                result.put(key.toString(), serializeObject(map.get(key)));
            }
        }

        return result;
    }

    private static Object serializeJavaBean(Object object)
    {
        Map<String,Object> result = new HashMap<String,Object>();

        PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(object.getClass());
        if (propertyDescriptors != null && propertyDescriptors.length != 0)
        {
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                if (propertyDescriptor.getName().compareTo("class") == 0) {
                    continue;
                }
                try {
                    result.put(propertyDescriptor.getName(), serializeObject(propertyDescriptor.getReadMethod().invoke(object)));
                } catch (Throwable t) {
                    // TODO What to do here?
                }
            }
        }

        return result;
    }

    private static PropertyDescriptor[] getPropertyDescriptors(Class clazz)
    {
        try {
            BeanInfo info = Introspector.getBeanInfo(clazz);
            return info.getPropertyDescriptors();
        } catch (IntrospectionException ie) {
            throw new RuntimeException("Could not examine class '" + clazz.getName() +
                "' using Introspector.getBeanInfo() to determine property information.", ie);
        }
    }

    //

    /**
     * TODO This is craphola, it only really works with the kind of objects that we use at Polar Rose. Really implement this properly soon.
     *
     * @param handlerDataClass
     * @param json
     * @return
     */

    @SuppressWarnings({"unchecked"})
    public static <T> T transmogrify(Class<T> handlerDataClass, Object json, Class elementType)
    {
        T instance = null;

        if (json != null)
        {
            // Primitive types

            if (handlerDataClass.isPrimitive())
            {
                instance = (T) json;
            }

            // If T is an Object then no generic type was specified and we simply return the JSON

            else if (handlerDataClass == Object.class)
            {
                instance = (T) json;
            }

            // Special type: URL

            else if (handlerDataClass == URL.class)
            {
                if (json.getClass() == String.class) {
                    try {
                        instance = (T) new URL((String) json);
                    } catch (MalformedURLException e) {
                        // Ignored
                    }
                }
            }

            // Easy case of Strings, Longs, Doubles, Booleans

            else if (handlerDataClass.isAssignableFrom(json.getClass()))
            {
                instance = (T) json;
            }

            else if (json.getClass().isArray())
            {
                List list = new ArrayList();

                Object[] array = (Object[]) json;
                for (Object o : array) {
                    list.add(transmogrify(elementType, o, null));
                }

                instance = (T) list;
            }

            // Assume a Java Bean

            else
            {
                try
                {
                    instance = handlerDataClass.newInstance();

                    Map<String, Object> map = (Map<String, Object>) json;

                    BeanInfo info = Introspector.getBeanInfo(handlerDataClass);
                    for (PropertyDescriptor propertyDescriptor : info.getPropertyDescriptors()) {
                        if (map.containsKey(propertyDescriptor.getName())) {
                            Class c = null;
                            if (List.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {
                                c = getComponentType(propertyDescriptor.getReadMethod().getGenericReturnType());
                            }
                            Object value = transmogrify(propertyDescriptor.getPropertyType(), map.get(propertyDescriptor.getName()), c);
                            if (value != null) {
                                propertyDescriptor.getWriteMethod().invoke(instance, value);
                            }
                        }
                    }
                }

                catch (Throwable t)
                {
                    if (log.isErrorEnabled()) {
                        log.warn("Cannot completely transmogrify JSON to an instance of " + handlerDataClass.getName() + ": " + t.getMessage(), t);
                    }
                }
            }
        }

        return instance;
    }

    public static Class getComponentType(Object genericType)
    {
        Class paramClass = null;

        if (genericType instanceof ParameterizedType)
        {
            ParameterizedType type = (ParameterizedType) genericType;

            if (type.getActualTypeArguments()[0] instanceof Class)
            {
                paramClass = (Class) type.getActualTypeArguments()[0];
            }
            else if (type.getActualTypeArguments()[0] instanceof WildcardType)
            {
                WildcardType wildcardType = (WildcardType) type.getActualTypeArguments()[0];

                if (wildcardType.getUpperBounds()[0] instanceof Class)
                {
                    paramClass = (Class) wildcardType.getUpperBounds()[0];
                }
            }
        }
        return paramClass;
    }
}
